package mspbots.teams.client.core.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.teams.client.oauth.AuthService;
import mspbots.teams.client.oauth.TokenState;
import mspbots.teams.common.QueryParams;
import mspbots.teams.common.RabbitKeys;
import mspbots.teams.common.ResultSync;
import mspbots.teams.common.annotation.ApiException;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * com.mspbots.api.client.WiseClient
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/11/12
 */
@Log4j2
@Service
public class TeamsClient {

    private final WebClient webClient;
    private final AuthService authService;
    private final ObjectMapper objectMapper;
    private final RabbitMessagingTemplate rabbitMessagingTemplate;

    public TeamsClient(WebClient.Builder builder, AuthService authService,
                       ObjectMapper objectMapper, RabbitMessagingTemplate rabbitMessagingTemplate) {
        this.webClient = builder.baseUrl("https://graph.microsoft.com")
                .defaultHeaders(httpHeaders -> httpHeaders.setContentType(MediaType.APPLICATION_JSON))
                .defaultHeaders(httpHeaders -> httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON)))
                .build();
        this.authService = authService;
        this.objectMapper = objectMapper;
        this.rabbitMessagingTemplate = rabbitMessagingTemplate;
    }

    public Mono<byte[]> getByte(String path, Map<String, Object> params) {
        return Mono.deferContextual(ctx -> {
            TokenState tokenState = ctx.get("tokenState");
            return this.authService.getToken(tokenState)
                    .flatMap(token -> this.webClient.get().uri(uriBuilder ->
                            uriBuilder.scheme("https").host("graph.microsoft.com").path(path).build(params))
                            .headers(httpHeaders -> httpHeaders.setBearerAuth(token.getAccessToken()))
                            .accept(MediaType.IMAGE_JPEG).retrieve().bodyToMono(byte[].class));
        });
    }

    public Mono<JsonNode> get(String path, Map<String, Object> pathParams) {
        return this.get(path, QueryParams.withDefault(), pathParams);
    }

    public Mono<JsonNode> get(String path, QueryParams queryParams, Map<String, Object> pathParams) {

        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl("https://graph.microsoft.com")
                .path(path).queryParams(queryParams);
        URI uri = uriComponentsBuilder.build().toUri();
        if (!ObjectUtils.isEmpty(pathParams)) {
            uri = uriComponentsBuilder.build(pathParams);
        }
        log.debug("Get uri: [{}]", uri.toString());
        URI finalUri = uri;
        return Mono.deferContextual(ctx -> this.authService.getToken(ctx.get("tokenState"))
                .flatMap(tokenState -> this.webClient.get()
                        .uri(finalUri)
                        .headers(httpHeaders -> httpHeaders.setBearerAuth(tokenState.getAccessToken()))
                        .retrieve()
                        .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                                .map(errMsg -> new ApiException(errMsg, clientResponse.statusCode())))
                        .bodyToMono(JsonNode.class))
                .timeout(Duration.ofMinutes(5)));
    }

    public Flux<JsonNode> fetch(String path, QueryParams queryParams, Map<String, Object> pathParams) {
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl("https://graph.microsoft.com")
                .path(path).queryParams(queryParams);

        URI uri = uriComponentsBuilder.build().toUri();

        if (!ObjectUtils.isEmpty(pathParams)) {
            uri = uriComponentsBuilder.build(pathParams);
        }

        return this.fetch(uri);
    }

    public Flux<JsonNode> fetch(URI url) {
        return this.request(url)
                .delayElement(Duration.ofSeconds(1))
                .expand(jsonNode -> {
                    JsonNode nextLinkNode = jsonNode.get("@odata.nextLink");
                    if (ObjectUtils.isEmpty(nextLinkNode)) {
                        return Mono.empty();
                    }
                    String nextUrl = nextLinkNode.textValue();
                    nextUrl = UriUtils.decode(nextUrl, Charset.defaultCharset());
                    return this.request(UriComponentsBuilder.fromUriString(nextUrl).build().toUri());
                });
    }

    private Mono<JsonNode> request(URI uri) {
        return Mono.deferContextual(ctx -> {
            TokenState tokenState = ctx.get("tokenState");
            ResultSync resultSync = ctx.getOrDefault("resultSync", ResultSync.withDefault(uri.getRawPath()));
            assert resultSync != null;
            resultSync.setElapsedTime(System.currentTimeMillis());
            resultSync.setTenantId(tokenState.getTenantId());
            resultSync.setParams(this.objectMapper
                    .convertValue(UriComponentsBuilder.fromUri(uri).build().getQueryParams(), JsonNode.class));
            return this.authService.getToken(tokenState).flatMap(token -> this.webClient.get()
                    .uri(uri).headers(httpHeaders -> httpHeaders.setBearerAuth(token.getAccessToken()))
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                            .map(errMsg -> new ApiException(errMsg, clientResponse.statusCode())))
                    .bodyToMono(JsonNode.class))
                    .timeout(Duration.ofMinutes(5))
                    .doOnSuccess(resp -> this.handlerSuccessNext(resp, resultSync))
                    .onErrorResume(err -> this.handlerException(err, resultSync));
        });
    }

    private Mono<JsonNode> handlerException(Throwable err, ResultSync resultSync) {
        if (err instanceof ApiException) {
            resultSync.setMsg(((ApiException) err).getMsg());
            resultSync.setStatus(((ApiException) err).getStatus());
        } else {
            resultSync.setMsg(err.getMessage());
            resultSync.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        resultSync.setElapsedTime(System.currentTimeMillis() - resultSync.getElapsedTime());

        log.error("Get {} data error action {}, elapsedTime {}, msg: {}",
                resultSync.getTenantId(), resultSync.getAction(),
                resultSync.getElapsedTime(), resultSync.getMsg());

        this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);
        return Mono.error(err);
    }

    private void handlerSuccessNext(JsonNode resp, ResultSync resultSync) {
        JsonNode countNode = resp.get("@odata.count");

        if (ObjectUtils.isEmpty(countNode)) {
            resultSync.setCount(resp.withArray("value").size());
        } else {
            resultSync.setCount(countNode.intValue());
        }

        resultSync.setElapsedTime(System.currentTimeMillis() - resultSync.getElapsedTime());

        log.info("Get {} data success, action {}, elapsedTime {}, count {}",
                resultSync.getTenantId(), resultSync.getAction(),
                resultSync.getElapsedTime(), resultSync.getCount());

        this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);
    }
}
